


(function() {
    "use strict";
    let __debug__ = false;
    const Cu  = Components.utils;
    function require(url) {
        let scope = {};
        Cu.import(url, scope);
        return scope;
    }
    let {MiscUtils, StrUtils} = require("chrome://jsm/content/utils.js");
    let {RuntimeError} = require("chrome://jsm/content/Exceptions.js");

    let connector = {
        handlers: new Map(),

        registerHandler(topic, handler) {
	    if (this.handlers.has(topic))
	        this.removeHandler(topic);

	    this.handlers.set(topic, function(msg) {
	        if (__debug__)
	            console.log("message for topic "+topic+" "+
	        		JSON.stringify(msg.data));
	        handler(msg.data, function(response) {
		    sendAsyncMessage(
		        "imacros:"+topic+"-response", 
		        typeof response != "undefined" ? response : {}
		    );
	        });
	    });
	    addMessageListener("imacros:"+topic, this.handlers.get(topic));
        },

        removeHandler(topic) {
	    if (!this.handlers.has(topic))
	        return;
	    removeMessageListener("imacros:"+topic, this.handlers.get(topic));
	    this.handlers.delete(topic);
        },

        postMessage(topic, data) {
	    sendAsyncMessage("imacros:"+topic, data);
        }
    };

    function stringify_error(e) {
        return {name: e.name, message: e.message, errnum: e.errnum};
    }



    
    const im_strre = "(?:\"(?:[^\"\\\\]+|\\\\[0btnvfr\"\'\\\\])*\"|\\S*)";

    
    function getInnerText(element, acc) {
        for (let node of element.childNodes) {
            if (node.nodeType == node.ELEMENT_NODE) {
                if (node.style.display == "none")
                    continue;   
                if (node.tagName.toLowerCase() == "script")
                    continue;   
                if (node.tagName.toLowerCase() == "br") {
                    
                    acc += "\n";
                    continue;
                }
                if (node.tagName.toLowerCase() == "pre") {
                    
                    acc += node.textContent;
                    continue;
                }
                acc += getInnerText(node, acc);
                if (node.tagName.toLowerCase() == "p")
                    acc += "\n\n"; 
            } else if (node.nodeType == node.TEXT_NODE) {
                acc += node.nodeValue.trim();
            }
        }

        return acc;
    }

    
    let TagHandler = {
        
        
        match(node, atts) {
            var match = true;

            for (var at in atts) {
                if (at == "txt") {
                    var txt = StrUtils.escapeTextContent(node.textContent);
                    if (!atts[at].exec(txt)) {
                        match = false; break;
                    }
                } else {
                    let atval = "", propval = "";
                    
                    if (at in node) {
                        propval = node[at];
                    } else if (at == "href" && "src" in node) {
                        
                        
                        propval = node.src;
                    }
                    
                    if (node.hasAttribute(at)) {
                        atval = node.getAttribute(at);
                    }
                    
                    if (!(!!atts[at].exec(propval) || !!atts[at].exec(atval))) {
                        match = false; break;
                    }
                } 
            }
            return match;
        },
        
        
        
        find(doc, root, pos, relative, tagName, atts, form_atts) {
            var xpath = "descendant-or-self", ctx = root, nodes = new Array();
            
            if (relative) {         
                xpath = pos > 0 ? "following" : "preceding";
                if (!(ctx = this.lastNode) || ctx.ownerDocument != doc)
                    return (this.lastNode = null);
            }
            xpath += "::"+tagName;
            
            let result = doc.evaluate(
	        xpath, ctx, null,
                content.XPathResult.ORDERED_NODE_ITERATOR_TYPE, null
	    );
            let node = null;
            while (node = result.iterateNext()) {
                nodes.push(node);
            }
            
            
            var count = 0, i, start, end, increment;
            if (pos > 0) {
                start = 0; end = nodes.length; increment = 1;
            } else if (pos < 0) {
                start = nodes.length-1; end = -1; increment = -1;
            } else {
                throw new BadParameter("POS=<number> or POS=R<number>"+
                                       " where <number> is a non-zero integer", 1);
            }

            
            if (form_atts && form_atts["name"] &&
                form_atts["name"].exec("NoFormName"))
                form_atts = null;

            
            for (i = start; i != end; i += increment) {
                
                
                let match = atts ? this.match(nodes[i], atts) : true;
                
                if (match && form_atts && nodes[i].form)
                    match = this.match(nodes[i].form, form_atts);
                if (match && ++count == Math.abs(pos)) {
                    
                    return (this.lastNode = nodes[i]);
                }
            }

            return (this.lastNode = null);
        },



        
        findByXPath(doc, root, xpath) {
            let nodes = new Array();
            
            try {
                var result = doc.evaluate(
		    xpath, root, null,
                    content.XPathResult.ORDERED_NODE_ITERATOR_TYPE, null
	        );
                let node = null;
                while (node = result.iterateNext()) {
                    nodes.push(node);
                }
            } catch (e) {
                throw new RuntimeError("incorrect XPath expression: "+
                                       xpath, 981);
            }
            if (nodes.length > 1)
                throw new RuntimeError("ambiguous XPath expression: "
                                       +xpath, 982);
            if (nodes.length == 1)
                return nodes[0];

            return null;
        },
        

        
        onExtractParam(tagName, element, extract_type) {
            var tmp = "", i;
            if (/^(txt|txtall)$/i.test(extract_type)) {
                tmp = RegExp.$1.toLowerCase();
                switch (tagName) {
                case "input": case "textarea":
                    return element.value;
                case "select":
                    if (tmp == "txtall") {
                        var s = new Array(), options = element.options;
                        for (i = 0; i < options.length; i++) {
                            s.push(options[i].text);
                        }
                        return s.join("[OPTION]");
                    } else {
                        
                        
                        return element.value;
                    }
                case "table":
                    tmp = "";
                    var rows = new Array();
                    for ( i = 0; i < element.rows.length; i++) {
                        let row = element.rows[i], ar = new Array();
                        for (var j = 0; j < row.cells.length; j++)
                            ar.push(getInnerText(row.cells[j], ""));
                        let s_row = '"'+ar.map(x => x.replace(/\"/g, '\"\"'))
                            .join('"'+player.dataSourceDelimiter+'"')+'"';
                        rows.push(s_row);
                    }
                    return rows.join(MiscUtils.isWindows() ? "\r\n": "\n");
                default:
                    return getInnerText(element, "");
                }
            } else if (/^htm$/i.test(extract_type)) {
                tmp = element.outerHTML;
                tmp = tmp.replace(/[\t\n\r]/g, " ");
                return tmp;
            } else if (/^href$/i.test(extract_type)) {
                if ("href" in element) 
                    return element["href"];
                else if (element.hasAttribute("href"))
                    return elem.getAttribute("href");
                else if ("src" in element)
                    return element["src"];
                else if (element.hasAttribute("src"))
                    return elem.getAttribute("src");
                else
                    return "#EANF#";
            } else if (/^(title|alt)$/i.test(extract_type)) {
                tmp = RegExp.$1.toLowerCase();
                if (tmp in element)
                    return element[tmp];
                else if (element.hasAttribute(tmp)) 
                    return elem.getAttribute(tmp);
                else
                    return "#EANF#";
            } else if (/^checked$/i.test(extract_type)) {
                if (!/^(?:checkbox|radio)$/i.test(element.type))
                    throw new BadParameter("EXTRACT=CHECKED makes sense"+
                                           " only for check or radio boxes");
                return element.checked ? "YES" : "NO";
            } else {
                throw new BadParameter("EXTRACT=TXT|TXTALL|HTM|"+
                                       "TITLE|ALT|HREF|CHECKED", 5);
            }
        },


        
        onContentParam(tagName, element, args) {
            
            this.htmlFocusEvent(element);
            
            switch (tagName) {
            case "select":
                
                
                this.handleSelectElement(element, args);
                this.htmlChangeEvent(element);
                break;
            case "input":
                switch(element.type) {
                case "file":
                    
                    
                    throw new Error("Sorry, upload functionality is currently not supported in iMacros for Firefox.");
                    break;
                case "text": case "hidden": 
                    
                case "color": case "date": case "datetime":
                case "datetime-local": case "email": case "month":
                case "number": case "range": case "search":
                case "tel": case "time": case "url": case "week":
                    element.value = args.txt;
                    this.htmlChangeEvent(element);
                    break;
                case "password":
                    this.handlePasswordElement(element, args.txt);
                    this.htmlChangeEvent(element);
                    break;
                case "checkbox":
                    if (/^(?:true|yes|on)$/i.test(args.txt)) {
                        if (!element.checked) 
                            element.click();
                    } else {
                        if (element.checked)
                            element.click();
                    }
                    break;
                default:
                    
                    this.simulateClick(element);
                }
                break;
            case "button":
                this.simulateClick(element);
                break;
            case "textarea":
                element.value = args.txt;
                this.htmlChangeEvent(element);
                break;
            default:
                
                
                this.simulateClick(element);
            }
            
            this.htmlBlurEvent(element);
        },


        
        handleSelectElement(element, args) {
            var options = element.options;

            
            if (element.multiple)
                element.options.selectedIndex = -1;
            
            if (args.cdata.type != "select")
                throw new RuntimeError(
                    "Unable to select entry(ies) specified by: "+
                        args.rawdata, 925);

            if (args.cdata.seltype =="all") {
                
                for (var j = 0; j < options.length; j++)
                    options[j].selected = true;
                return;
            } 
            
            if (args.cdata.seltype == "multiple") 
                element.multiple = true;

            for (var i = 0; i < args.cdata.opts.length; i++) {
                switch (args.cdata.opts[i].typ) {
                case "$": case "%":
                    var re = new RegExp(args.cdata.opts[i].re_str, "i");
		    var found = false;
                    for (var j = 0; j < options.length; j++) {
                        var o = options[j];
                        var s = (args.cdata.opts[i].typ == "$") ?
                            StrUtils.escapeTextContent(o.text) : o.value;
                        if (re.exec(s)) {
                            found = true;
                            options[j].selected = true;
                            break;
                        }
                    }
                    if (!found) {
                        throw new RuntimeError(
                            "Entry ["+args.cdata.opts[i].str+"] not available"+
                                " [Box has "+options.length+" entries]", 924);
                    }
                    break;
                case "#": 
                    if (args.cdata.opts[i].idx > element.length)
                        throw new RuntimeError(
                            "Entry with index "+args.cdata.opts[i].idx+
                                " not available [Box has "+element.length+
                                " entries]", 924);
                    options[args.cdata.opts[i].idx-1].selected = true;
                    break;
                }
            }
        },

        
        handlePasswordElement(element, content) {
            element.value = content;
        },

        
        simulateClick(element) {
            if (typeof(element.click) == "function") {
                element.click();
            } else {
                var initEvent = function(e, d, typ) {
                    e.initMouseEvent(typ, true, true, d.defaultView, 1, 0, 0, 0, 0,
                                     false, false, false, false, 0, null);
                };
                var stop = function (e) { e.stopPropagation(); };

                var doc = element.ownerDocument, x;
                var events = { "mouseover": null,
                               "mousedown": null,
                               "mouseup"  : null,
                               "click"    : null };

                element.addEventListener("mouseover", stop, false);
                element.addEventListener("mouseout", stop, false);
                
                for (x in events) {
                    events[x] = doc.createEvent("MouseEvent");
                    initEvent(events[x], doc, x);
                    element.dispatchEvent(events[x]);
                }
            }
        },

        
        htmlChangeEvent(element) {
            if (!/^(?:input|select|textarea)$/i.test(element.tagName))
                return;
            var evt = element.ownerDocument.createEvent("Event");
            evt.initEvent("change", true, false);
            element.dispatchEvent(evt);
        },

        
        htmlFocusEvent(element) {
            if (!/^(?:a|area|label|input|select|textarea|button)$/i.
                test(element.tagName))
                return;
            var evt = element.ownerDocument.createEvent("Event");
            evt.initEvent("focus", false, false);
            element.dispatchEvent(evt);
        },

        
        htmlBlurEvent(element) {
            if (!/^(?:a|area|label|input|select|textarea|button)$/i.
                test(element.tagName))
                return;
            var evt = element.ownerDocument.createEvent("Event");
            evt.initEvent("blur", false, false);
            element.dispatchEvent(evt);
        },

        onEventParam(element, type) {
	    switch(type) {
	    case "saveitem": case "savepictureas": 
	    case "savetargetas": case "savetarget":
	        let e = element;
	        while(e && e.nodeType == e.ELEMENT_NODE &&
		      !(e.hasAttribute("href") || e.hasAttribute("src"))
		     )
		    e = e.parentNode;
	        if (!e || e.nodeType != e.ELEMENT_NODE)
		    throw new RuntimeError(
		        "Can not find link to save target", 923
		    );
	        return e.href || e.src;
	    case "save_element_screenshot":
	        var rect = element.getBoundingClientRect();
	        var win = element.ownerDocument.defaultView;
	        var doc = win.document;
                var doc_el = doc.documentElement;
                var body = doc.body;
                
                var clientTop = doc_el.clientTop || body.clientTop || 0;
                var clientLeft = doc_el.clientLeft || body.clientLeft || 0;
                var scrollX = win.scrollX || doc_el.scrollLeft || body.scrollLeft;
                var scrollY = win.scrollY || doc_el.scrollTop || body.scrollTop;

                var x1 = Math.round(rect.left + scrollX - clientLeft);
                var y1 = Math.round(rect.top  + scrollY - clientTop);

	        var canvas = doc.createElementNS("http://www.w3.org/1999/xhtml",
					         "canvas");
	        canvas.style.width = rect.width+"px";
	        canvas.style.height = rect.height+"px";
	        canvas.width = rect.width;
	        canvas.height = rect.height;
	        var ctx = canvas.getContext("2d");
	        ctx.clearRect(0, 0, rect.width, rect.height);
	        ctx.save();
	        ctx.drawWindow(win, x1, y1, rect.width, rect.height, "rgb(0,0,0)");
	        ctx.restore();

	        return canvas.toDataURL("image/png", 1.0);
	    }
        },

    };



    function FrameScriptPlayer() {
        this.onError = function(err) {
            var obj = {
	        name: "ScriptError",
	        message: err.message+" on "+err.filename+":"+err.lineno
            }
            connector.postMessage("error-occurred", obj);
        };
        this.targetElement = null;
        this.registerHandlers();
        this.dataSourceDelimiter = ",";
    }


    FrameScriptPlayer.prototype.registerHandlers = function() {
        this._handleTagCommand = this.handleTagCommand.bind(this);
        this._handleSaveAsCommand = this.handleSaveAsCommand.bind(this);
        this._handleSearchCommand = this.handleSearchCommand.bind(this);
        this._onHighlightImage = this.onHighlightImage.bind(this);
        this._handleFrameCommand = this.handleFrameCommand.bind(this);
        this._handleCapturePage = this.handleCapturePage.bind(this);
        this._onLocateElement = this.onLocateElement.bind(this);
        this._handleEventCommand = this.handleEventCommand.bind(this);
        this._handleEventsCommand = this.handleEventsCommand.bind(this);
        this._handleClickCommand = this.handleClickCommand.bind(this);
        this._onSetDataSourceDelimiter =
            this.onSetDataSourceDelimiter.bind(this);
        addMessageListener("imacros:enable-player", msg => {
	    if (__debug__)
	        console.debug("enable-player, time="+msg.data.time);
	    connector.registerHandler("tag-command", this._handleTagCommand);
            connector.registerHandler("click-command",
                                      this._handleClickCommand);
	    connector.registerHandler("saveas-command",
                                      this._handleSaveAsCommand);
	    connector.registerHandler("search-command",
                                      this._handleSearchCommand);
	    connector.registerHandler("highlight-image",
                                      this._onHighlightImage);
	    connector.registerHandler("frame-command",
                                      this._handleFrameCommand);
	    connector.registerHandler("capture-page",
                                      this._handleCapturePage);
            connector.registerHandler("locate-element",
                                      this._onLocateElement);
            connector.registerHandler("event-command",
                                      this._handleEventCommand);
            connector.registerHandler("events-command",
                                      this._handleEventsCommand);
            connector.registerHandler("set-datasource-delimiter",
                                      this._onSetDataSourceDelimiter);
	    addEventListener("error", this.onError);
	    sendAsyncMessage("imacros:enable-player-response", {});
        });

        addMessageListener("imacros:disable-player", msg => {
	    connector.removeHandler("tag-command", this._handleTagCommand);
            connector.removeHandler("click-command",
                                    this._handleClickCommand);
	    connector.removeHandler("saveas-command",
                                    this._handleSaveAsCommand);
	    connector.removeHandler("search-command",
                                    this._handleSearchCommand);
	    connector.removeHandler("hightlight-image", 
                                    this._handleImageSearchCommand);
	    connector.removeHandler("frame-command", this._handleFrameCommand);
	    connector.removeHandler("capture-page", this._handleCapturePage);
            connector.removeHandler("locate-element", this._onLocateElement);
            connector.removeHandler("event-command", this._handleEventCommand);
            connector.removeHandler("events-command",
                                    this._handleEventsCommand);
            connector.removeHandler("set-datasource-delimiter",
                                    this._onSetDataSourceDelimiter);
            this.targetElement = null;
	    removeEventListener("error", this.onError);
	    sendAsyncMessage("imacros:disable-player-response", {});
        });
    };

    
    FrameScriptPlayer.prototype.findFrame = function(win, obj) {
        if (obj.num == 0)
            return win;
        var frames = win.frames, i, f;
        for (i = 0; i < frames.length; i++) {
            var dv = frames[i];
            if (--obj.num == 0) {
                return frames[i];
            } else if (f = this.findFrame(dv, obj))
                return f;
        }
        return null;
    };

    
    FrameScriptPlayer.prototype.findFrameByName = function(win, name) {
        var frames = win.frames, i, f;
        for (var i = 0; i < frames.length; i++) {
            var dv = frames[i];
            if (name.test(frames[i].name))
                return frames[i];
            else if (f = this.findFrameByName(dv, name))
                return f;
        }
        return null;
    }

    FrameScriptPlayer.prototype.handleFrameCommand = function(args, callback) {
        var f = null;
        if (typeof(args.number) == "number") {
            f = this.findFrame(content, {num: args.number});
        } else if (args.name) {
            var name_re = new RegExp("^"+args.name.replace(/\*/g, ".*")+"$");
            f = this.findFrameByName(content, name_re);
        }
        if (__debug__)
	    console.log("handleFrame: args="+JSON.stringify(args)+
		        ", frame "+(f? "found" : "not found"));
        callback({found: !!f});
    };


    FrameScriptPlayer.prototype.highlightElement = function(element) {
        element.style.outline = "1px solid blue";
    };



    FrameScriptPlayer.prototype.findElementPosition = function(element) {
        var rect = element.getBoundingClientRect();
        return {
            x: Math.round(rect.left+rect.width/2),
            y: Math.round(rect.top+rect.height/2),
        }
    };

    FrameScriptPlayer.prototype.onSetDataSourceDelimiter = function(args, callback) {
        this.dataSourceDelimiter = args.delimiter;
        callback();
    };

    FrameScriptPlayer.prototype.handleTagCommand = function(args, callback) {
        var win = content;
        if (args.frame) {
	    if (typeof(args.frame.number) == "number") {
	        win = this.findFrame(content, {num: args.frame.number});
	    } else if (args.frame.name) {
	        var name_re = new RegExp(
		    "^"+args.frame.name.replace(/\*/g, ".*")+"$"
	        );
                win = this.findFrameByName(content, name_re);
	    }
        }
        var doc = win.document;
        var root = doc.documentElement;
        var element;

        var retobj = {
            found: false,       
            extract: "",        
            error: null         
        };
        if (__debug__)
	    console.log("playing tag comand args="+JSON.stringify(args)+
		        ", page="+win.location.toString());
        try {
            
            if (args.atts)
                for (var x in args.atts) 
                    args.atts[x] = new RegExp(args.atts[x], "i");
            if (args.form)
                for (var x in args.form) 
                    args.form[x] = new RegExp(args.form[x], "i");

            if (args.xpath)
                element = TagHandler.findByXPath(doc, root, args.xpath);
            else 
                element = TagHandler.find(doc, root, args.pos, args.relative,
                                          args.tagName, args.atts, args.form);
            if (!element) {
                var msg = "element "+(args.xpath ? "" : args.tagName.toUpperCase())+
                    " specified by "+(args.xpath ? args.xpath : args.atts_str)+
                    " was not found";
                if (args.type == "extract") {
                    retobj.extract = "#EANF#";
                } else {
                    retobj.error = stringify_error(new RuntimeError(msg, 921));
                }
                callback(retobj);
                return;
            }
            retobj.found = true;
            
            if (args.scroll) {
                element.scrollIntoView()
            }

            if (args.highlight) {
                this.highlightElement(element);
            }

            if (args.tagName == "*" || args.tagName == "")
                args.tagName = element.tagName.toLowerCase();
            
            if (args.type == "extract") {
                retobj.extract =
                    TagHandler.onExtractParam(args.tagName, element, args.txt);
            } else if (args.type == "content") {
                if (args.cdata.type == "event") {
		    switch(args.cdata.etype) {
		    case "saveitem": case "savepictureas": 
		    case "savetargetas": case "savetarget":
                    case "save_element_screenshot":
		        retobj.targetURI = TagHandler.onEventParam(
			    element, args.cdata.etype
		        );
		        break;
                    case "mouseover":
                        var evt = doc.createEvent("MouseEvent");
                        evt.initMouseEvent("mouseover", true, true,
                                           doc.defaultView, 0, 0, 0, 0, 0,
                                           false, false, false, false, 0, null);
                        element.dispatchEvent(evt);
                        break;
                    case "fail_if_found":
                        retobj.error = new RuntimeError("FAIL_IF_FOUND event", 990);
                        break;
                    default:
                        retobj.error = "Unknown event type "+args.cdata.etype+
                            " for tag command";
                    }
                } else {
                    TagHandler.onContentParam(args.tagName, element, args);
                }
            } else {
                
                
                if (args.download_pdf && element.tagName == "A" &&
                    /\.pdf$/i.test(element.href))
                    retobj.targetURI = element.href;
                else
                    TagHandler.onContentParam(args.tagName, element);
            }
        } catch (e) {
	    Components.utils.reportError(e);
            retobj.error = stringify_error(e);
        } finally {
	    if (__debug__)
                console.debug("handleTagCommand, retobj="+JSON.stringify(retobj));
            callback(retobj);
        }
    };

    FrameScriptPlayer.prototype.handleClickCommand = function(args, callback) {
        try {
            let doc = content.document;
            var details = {
                doc: doc,
                point: {x: args.x, y: args.y},
                clickCount: 1,
                button: 0,
                target: doc.elementFromPoint(args.x, args.y) || doc.documentElement
            };
            details.type = "mousedown";
            this.dispatchMouseEvent(details);
            details.type = "mouseup";
            this.dispatchMouseEvent(details);
            if (args.cdata) {
                TagHandler.onContentParam(
                    details.target.tagName.toLowerCase(),
                    details.target, args.cdata
                );
            }
            callback();
        } catch(e) {
            Cu.reportError(e);
            callback({error: stringify_error(e)});
        }
        
    };

    FrameScriptPlayer.prototype.onLocateElement = function(args, callback) {
        var win = content;
        if (args.frame) {
	    if (typeof(args.frame.number) == "number") {
	        win = this.findFrame(content, {num: args.frame.number});
	    } else if (args.frame.name) {
	        var name_re = new RegExp(
		    "^"+args.frame.name.replace(/\*/g, ".*")+"$"
	        );
                win = this.findFrameByName(content, name_re);
	    }
        }
        var doc = win.document;
        this.targetElement = null;
        if (args.type == "xpath") {
            this.targetElement = TagHandler.findByXPath(
                doc, doc.documentElement, args.selector
            );
        } else if (args.type == "selector") {
            this.targetElement = doc.querySelector(args.selector);
        } else {
            this.targetElement = doc.documentElement;
        }
        if (!this.targetElement)
            callback({found: false});
        else
            callback({found: true, type: this.targetElement.type || null})
    };

    FrameScriptPlayer.prototype.dispatchMouseEvent = function(details) {
        var ctrlKey = /ctrl/i.test(details.modifiers);
        var altKey = /alt/i.test(details.modifiers);
        var shiftKey = /shift/i.test(details.modifiers);
        var metaKey = /meta/i.test(details.modifiers);
        var clickCount = details.clickCount || 1;
        if (details.type == "mousemove")
            clickCount = 0;

        var clientX, clientY, pageX, pageY;
        if (!details.point) {
            var rect = details.target.getBoundingClientRect();
            clientX = Math.round((rect.left+rect.right)/2);
            clientY = Math.round((rect.top+rect.bottom)/2);
            pageX = clientX + details.doc.defaultView.scrollX;
            pageY = clientY + details.doc.defaultView.scrollY;
        } else {
            pageX = details.point.x;
            pageY = details.point.y;
            if (/HTMLHtmlElement/.test(details.target)) {
                details.target = details.doc.elementFromPoint(pageX, pageY);
            }
            clientX = pageX - details.doc.defaultView.scrollX;
            clientY = pageY - details.doc.defaultView.scrollY;
        }
        
        var screenX = details.doc.defaultView.mozInnerScreenX+clientX;
        var screenY = details.doc.defaultView.mozInnerScreenY+clientY;
        var relatedTarget = null;

        if (details.type == "mousedown") {
            
            var mover = details.doc.createEvent("MouseEvent");
            mover.initMouseEvent("mouseover", true, true,
                                 details.doc.defaultView, clickCount,
                                 screenX, screenY, clientX, clientY,
                                 ctrlKey, altKey, shiftKey, metaKey,
                                 details.button, relatedTarget);
            details.target.dispatchEvent(mover);
        }

        var event = details.doc.createEvent("MouseEvent");
        event.initMouseEvent(details.type, true, true,
                             details.doc.defaultView,
                             clickCount, screenX, screenY, clientX, clientY,
                             ctrlKey, altKey, shiftKey, metaKey,
                             details.button, relatedTarget);

        details.target.dispatchEvent(event);

        if (details.type == "mousedown") {
            
            if (typeof details.target.focus == "function")
                details.target.focus();
            
            
            if (/HTMLOptionElement/.test(details.target) &&
                /HTMLSelectElement/.test(details.target.parentNode)) {
                if (!details.target.parentNode.multiple) {
                    details.target.parentNode.selectedIndex =
                        details.target.index;
                } else {
                    details.target.selected = true;
                }
                
                var change = details.doc.createEvent("Event");
                change.initEvent("change", true, true);
                details.target.dispatchEvent(change);
            }
        } else if (details.type == "mouseup") {
            
            
            
            
            var click = details.doc.createEvent("MouseEvent");
            click.initMouseEvent(clickCount == 1 ? "click" : "dblclick",
                                 true, true,
                                 details.doc.defaultView, clickCount,
                                 screenX, screenY, clientX, clientY,
                                 ctrlKey, altKey, shiftKey, metaKey,
                                 details.button, relatedTarget);
            details.target.dispatchEvent(click);
        } 
    };


    FrameScriptPlayer.prototype.dispatchKeyboardEvent = function(details) {
        var event = details.doc.createEvent("KeyboardEvent");
        var ctrlKey = /ctrl/i.test(details.modifiers);
        var altKey = /alt/i.test(details.modifiers);
        var shiftKey = /shift/i.test(details.modifiers);
        var metaKey = /meta/i.test(details.modifiers);
        var keyCode = details.key;
        var charCode = details.char ? details.char.charCodeAt(0) : 0;
        event.initKeyEvent(details.type, true, true,
                           details.doc.defaultView,
                           ctrlKey, altKey, shiftKey, metaKey,
                           keyCode, charCode);
        
        details.target.dispatchEvent(event);
    };
    
    FrameScriptPlayer.prototype.handleEventCommand = function(args, callback) {
        let doc = this.targetElement.ownerDocument;
        if (typeof this.targetElement.focus == "function")
            this.targetElement.focus();
        if (args.type.startsWith("mouse")) {
            let details = {
                doc: doc,
                target: this.targetElement,
                type: args.type,
                point: args.point,
                button: args.button,
                modifiers: args.modifiers
            };
            this.dispatchMouseEvent(details);
        } else if (/^key/i.test(args.type)) {
            let details = {
                doc: doc,
                target: this.targetElement,
                type: args.type,
                key: args.key,
                char: args.char,
                modifiers: args.modifiers
            };
            this.dispatchKeyboardEvent(details);
        } else if (args.type == "click") {
            
            let details = {
                doc: doc,
                target: this.targetElement,
                point: args.point,
                button: args.button,
                clickCount: 1,
                modifiers: args.modifiers
            };
            details.type = "mousedown";
            this.dispatchMouseEvent(details);
            details.type = "mouseup";
            this.dispatchMouseEvent(details);
        } else if (args.type == "dblclick") {
            
            let details = {
                doc: doc,
                target: this.targetElement,
                point: args.point,
                button: args.button,
                modifiers: args.modifiers
            };
            details.clickCount = 1;
            details.type = "mousedown";
            this.dispatchMouseEvent(details);
            details.type = "mouseup";
            this.dispatchMouseEvent(details);
            details.clickCount = 2;
            details.type = "mousedown";
            this.dispatchMouseEvent(details);
            details.type = "mouseup";
            this.dispatchMouseEvent(details);
        }

        if (args.highlight)
            this.highlightElement(this.targetElement);
        callback();
    };

    FrameScriptPlayer.prototype.handleEventsCommand = function(args, callback){
        let doc = this.targetElement.ownerDocument;
        if (typeof this.targetElement.focus == "function")
            this.targetElement.focus();
        let g = null;    
        let player = this;
        if (/mousemove/i.test(args.type)) {
            let details = {doc: doc, target: this.targetElement,
                           type: args.type, modifiers: args.modifiers};
            g = (function *fireEvent (details, points) {
                while(points.length) {
                    details.point = points.shift();
                    player.dispatchMouseEvent(details);
                    yield true;
                }
                yield false;
            }) (details, args.points);
        } else if (/^keypress/i.test(args.type)) {
            let details = {doc: doc, target: this.targetElement,
                           modifiers: args.modifiers};
            g = (function *fireEvent (details, keys, chars) {
                let typ = keys.length ? "key" : "char";
                while((typ == "key" ? keys : chars).length) {
                    if (typ == "key") {
                        
                        details.key = keys.shift();
                        details.type = "keydown";
                        player.dispatchKeyboardEvent(details);
                        details.type = "keypress";
                        player.dispatchKeyboardEvent(details);
                        details.type = "keyup";
                        player.dispatchKeyboardEvent(details);
                    } else {
                        let char = chars.charAt(0);
                        chars = chars.substring(1);
                        
                        
                        let key = 65; 
                        if (/[A-Z]/i.test(char)) {
                            
                            key = char.toUpperCase().charCodeAt(0);
                        }
                        details.key = key;
                        details.type = "keydown";
                        player.dispatchKeyboardEvent(details);
                        details.type = "keypress";
                        details.char = char;
                        player.dispatchKeyboardEvent(details);
                        details.key = key;
                        details.type = "keyup";
                        player.dispatchKeyboardEvent(details);
                    }
                    yield true;
                }
                yield false;
            }) (details, args.keys, args.chars);
        }
        let __eventsInterval = setInterval(() => {
            let val = g.next().value;
            if (!val) {
                clearInterval(__eventsInterval);
                callback();
                if (args.highlight)
                    this.highlightElement(this.targetElement);
            }
        }, 0);
    };

    FrameScriptPlayer.prototype.handleSaveAsCommand = function(args, callback) {
        
        
        
        
        
        
        
        let walker = content.document.createTreeWalker(
	    content.document.documentElement, 
	    content.NodeFilter.SHOW_TEXT, {
	        acceptNode: function (node) {
		    if (node.parentElement.style.visibility == "hidden" ||
		        node.parentElement.style.display == "none" ||
		        node.parentElement.tagName == "SCRIPT" ||
		        node.parentElement.tagName == "NOSCRIPT" ||
		        node.parentElement.tagName == "STYLE")
		        return content.NodeFilter.FILTER_REJECT;

		    return content.NodeFilter.FILTER_ACCEPT;
	        }
	    });
        let s = '';
        while (walker.nextNode() != null)
	    s += walker.currentNode.textContent;
        
        callback(s);
    };



    FrameScriptPlayer.prototype.handleSearchCommand = function(args, callback) {
        var search_re, retobj = {found: false}, query = args.query;
        try {
            switch (args.type) {
            case "txt":
                
                query = StrUtils.escapeREChars(query);
                
                query = query.replace(/\*/g, '(?:[\r\n]|.)*');
                
                query = query.replace(/ /g, "\\s+");
                search_re = new RegExp(query, args.ignore_case);
                break;
            case "regexp":
                try {
                    search_re = new RegExp(query, args.ignore_case);
                } catch(e) {
                    console.error(e);
                    throw new RuntimeError("Can not compile regular expression: "
                                           +query, 983);
                }
                break;
            }

            var win = content;
	    if (args.frame) {
	        if (args.frame.number != 0) {
		    win = this.findFrame(content, {num: args.frame.number});
	        } else if (args.frame.name) {
		    var name_re = new RegExp(
		        "^"+args.frame.name.replace(/\*/g, ".*")+"$"
		    );
		    win = this.findFrameByName(content, name_re);
	        }
	    }
            var root = win.document.documentElement;
            var found = search_re.exec(root.innerHTML);
            if (!found) {
                throw new RuntimeError(
                    "Source does not match to "+args.type.toUpperCase()+"='"+
                        args.query+"'", 926
                );
            }
            retobj.found = true;
            if (args.extract) {
                retobj.extract = args.extract.
                    replace(/\$(\d{1,2})/g, function (match_str, x) {
                        return found[x];
                    });
            }
        } catch(e) {
            retobj.error = stringify_error(e);
            console.error(e);
        } finally {
            callback(retobj);
        }
    };


    FrameScriptPlayer.prototype.webPageToDataURL = function(
        x, y, width, height, imageType, callback
    ) {
        var canvas = content.document.createElementNS(
	    "http://www.w3.org/1999/xhtml", "canvas"
        );
        canvas.style.width = width+"px";
        canvas.style.height = height+"px";
        canvas.width = width;
        canvas.height = height;
        var ctx = canvas.getContext("2d");
        ctx.clearRect(x, y, width, height);
        ctx.save();
        ctx.drawWindow(content, x, y, width, height, "rgb(0,0,0)");
        ctx.restore();
        callback(canvas.toDataURL(imageType, 1.0));
    };

    FrameScriptPlayer.prototype.onHighlightImage = function(args, callback) {
        var div = content.document.createElement("div");
        div.style.width = args.width+"px";
        div.style.height = args.height+"px";
        div.style.border = "1px solid #9bff9b";
        div.style.zIndex = "100";
        div.style.position = "absolute";
        div.style.left = Math.floor(args.x-args.width/2)+"px";
        div.style.top = Math.floor(args.y-args.height/2)+"px";
        content.document.body.appendChild(div);
        callback();
    };


    FrameScriptPlayer.prototype.handleCapturePage = function(args, callback) {
        var width = content.innerWidth + content.scrollMaxX;
        var height = content.innerHeight + content.scrollMaxY;
        
        if (width > 10000)
            width = 10000;
        if (height > 10000)
            height = 10000;
        this.webPageToDataURL(0, 0, width, height, args.type, callback);
    };


    var player = new FrameScriptPlayer();


})();
